package model;

import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

import rmi.client.Client;
import rmi.server.ServerRemote;
import toolkit.Debug;
import exception.DrawException;
import exception.GameRoundEndException;

abstract public class GameRound
{
	protected GameRoundState currentState;

	protected ArrayList<Participant> gameRoundParticipants;
	
	protected ArrayList<Participant> playingParticipants;
	
	protected ArrayList<Participant> winners;
	
	protected final Table table;
	
	public final static Comparator<Participant> participantBetSorter = new Comparator<Participant>()
	{
		public int compare(Participant p1, Participant p2)
		{
			// 1 = down, -1 = up
			return p1.getBetContainer().getBet() >= p2.getBetContainer().getBet() ? 1 : -1;
		}
	};
	
	public GameRound(Table table)
	{
		this.table = table;
		
		if (!Debug.isTextual())
		{
			ServerRemote.getInstance().newGameRound(getGameId());
		}
		
		ArrayList<Participant> tempParticipants  =  new ArrayList<Participant>();
		
		for (Participant participant: table.getParticipants())
		{
			if(participant.getBetContainer().getChips()!=0)
			{
				tempParticipants.add(participant);
			}
		}
		
		
		// The participants that are still playing in this game round (people who did not fold).
		gameRoundParticipants = new ArrayList<Participant>(tempParticipants);
		
		// The participants that still can choose which action to take.
		playingParticipants = new ArrayList<Participant>(tempParticipants);
	}
	
	public Game getGame()
	{
		return table.getGame();
	}
	
	public int getGameId()
	{
		return getGame().getId();
	}
	
	public GameRoundState getCurrentState()
	{
		return currentState;
	}
	
	public void drawBoardCard() throws DrawException
	{
		table.getBoard().addCard(table.getCardStack().drawCard());
		
		updateBoard();
	}

	public void drawBoardCards(int amount) throws DrawException
	{
		table.getBoard().addCards(table.getCardStack().drawCards(amount));
		
		updateBoard();
	}

	private void updateBoard()
	{
		if (!Debug.isTextual())
		{
			ServerRemote.getInstance().updateBoard(getGameId(), table.getBoard().getCards());
		}
	}
	
	public void drawHandCard() throws DrawException
	{
		for (Participant participant: playingParticipants)
		{
			participant.getHand().addCard(table.getCardStack().drawCard());
			
			updateHand(participant);
		}
	}

	public void drawHandCards(int amount) throws DrawException
	{
		for (Participant participant: playingParticipants)
		{
			participant.getHand().addCards(table.getCardStack().drawCards(amount));
			
			updateHand(participant);
		}
	}
	
	private void updateHand(Participant participant)
	{
		if (!Debug.isTextual())
		{
			ServerRemote.getInstance().updateHand(getGameId(), participant);
		}
	}
	
	public ArrayList<Participant> getGameRoundParticipants()
	{
		return gameRoundParticipants;
	}

	public ArrayList<Participant> getPlayingParticipants()
	{
		return playingParticipants;
	}
	
	abstract protected ChosenAction determineAction(int actionsPerformed, Participant participant, int highestStateBet);
	
	public void chooseAction() throws GameRoundEndException
	{
		Debug.log(getCurrentState(), "state");
		
		Debug.printLine();
		
		// We start at position 1, because the dealer is always skipped the first time.
		int offset = 1;
		
		int size = playingParticipants.size();

		if(size==1)
		{
			offset=0;
		}
		if (size == 0)
		{
			return;
		}
		
		int limit = size;
		
		int actionsPerformed = 0;
		
		int index = offset;
		
		// The highest bet made this round.
		int highestStateBet = 0;
		
		// Because we loop some parts of the list multiple times
		// we need to keep track when to break the loop.
		boolean finished = false;
		
		int iterations = 0;
		
		while (true)
		{
			// Should the player be removed from the list?
			boolean remove = false;
            
			Participant participant = playingParticipants.get(index);
			
			Debug.log(participant, "participant");
			Debug.log(participant.getPositionString(), "position");
			
			BetContainer betContainer = participant.getBetContainer();
			
			 //if (participant.getBetContainer().getChips() == 0) remove = true;
			
			// Determines which action the participant should take.
			// It includes all possible exceptions that can be made.
			// Because this is game variant and type specific we made it a abstract method.
			ChosenAction chosenAction = determineAction(actionsPerformed, participant, highestStateBet);
			
			// Dirty fix...
			if (actionsPerformed == 1 && chosenAction.isDeterminedByRules())
			{
				offset++;
			}
			
			Action action = chosenAction.getAction();
			
			boolean extraLoop = action == Action.RAISE;
			
			if (extraLoop)
			{
				action = Action.CALL;
			}
	
			// When the action is raise, he should first call in order to raise his
			// amount of chips like the other players.
			actionLoop: do
			{
				switch (action)
				{
					case CALL:
						
						// If the participants bet is lower than the current bet.
						if (betContainer.getStateBet() < highestStateBet)
						{
							// The difference between the amount he already has bet
							// and the the highest bet to the main pot.
							int difference = highestStateBet - betContainer.getStateBet();
							
							// Does the participant have enough chips?
							if (betContainer.getChips() < difference)
							{
								// Add the amount he has left to his bet, so he goes all in.
								betContainer.allIn();
								
								// People who went all in do not have to be looped again.
								//remove = true;
								
								break actionLoop;
							}
							else
							{	
								// Equals the state bet of the participant with the highest state bet.
								betContainer.setStateBet(highestStateBet);
								
								// Remove the chips needed to make them equal.
								betContainer.removeChips(difference);
							}
						}
						
						break;
					
					case RAISE:
						
						Debug.log(chosenAction.getAmountRaised(), "raised with");
						
						// Does the participant have enough chips?
						if (chosenAction.getAmountRaised() >= betContainer.getChips())
						{
							// Add the maximum amount of chips the participant can pay, so he goed all in.
							betContainer.allIn();
							
							highestStateBet = betContainer.getStateBet();
							
							// People we went all in do not have to be looped again.
							//remove = true;
							
							break actionLoop;
						}
						else
						{
							// Add the amount the participant has chosen to raise to his bet.
							betContainer.addStateBet(chosenAction.getAmountRaised());
							
							// Remove those chips.
							betContainer.removeChips(chosenAction.getAmountRaised());
						}
	
						// This bet is the new highest state bet.
						highestStateBet = betContainer.getStateBet();
					    
						// If this participant is the first of the loop we do not have to loop a part afterwards
						// to have checked all participants.
						if (index != 0)
						{
							finished = false;
							
							offset = index;
						}
						
						// Restart or stop the loop at the last participant.
						limit = playingParticipants.size();
						
						break;
					
					case FOLD:
						
						gameRoundParticipants.remove(participant);
						
						// People we have folded do not have to be looped again.
						remove = true;
						
						break actionLoop;
				}
				
				if (extraLoop)
				{
					action = chosenAction.getAction();
					
					extraLoop = false;
				}
				else
				{
					break;
				}
			}
			while (true);

			Debug.log(action, "action");
			Debug.log(betContainer.getStateBet(), "bet");
			Debug.log(betContainer.getChips(), "chips");
			
			ServerRemote.getInstance().actionPerformed(participant);
			
			actionsPerformed++;
			
			if (remove)
			{
				playingParticipants.remove(participant);
				
				if (playingParticipants.size() == 1)
				{
					addStateBetToGameRoundBet();
					
					playingParticipants.clear();
					
					return;
				}
				
				index--;
				
				limit--;
			}
			
			// Keep track of how many iterations are made.
			iterations++;
			
			if (++index == limit)
			{
				if (!finished)
				{
					index = 0;
					
					limit = offset;
					
					finished = true;
				}
				else
				{
					break;
				}
			}
			
			Debug.printLine();
		}
		
		Debug.printSolidLine();
		
		addStateBetToGameRoundBet();
		
		determinePots();
	}
	
	public void addStateBetToGameRoundBet()
	{
		// Add the state bet to the game round bet.
		for (Participant participant: table.getParticipants())
		{
			BetContainer betContainer = participant.getBetContainer();
			
			betContainer.addBet(betContainer.getStateBet());
			betContainer.setStateBet(0);
		}
	}
	
	public void showHandCards()
	{
		for (Participant participant: table.getParticipants())
		{
			// TODO: if it is not auto muck let the user decide by input.
			if (participant.getPlayer().getSettings().useAutoMuck())
			{
				participant.showHandCards();
			}
		}
	}
	
	public ArrayList<Participant> getWinners()
	{
		return winners;
	}
	
	public void determinePots()
	{
		table.emptyPots();
		
		Collections.sort(gameRoundParticipants, participantBetSorter);
		
		// Determines the total bet at this moment. Is for debugging only.
		int total = 0;
		
		for (Participant p: table.getParticipants())
		{
			total += p.getBetContainer().getBet();
		}
		
		Debug.printSolidLine();
		
		Debug.log(total, "total bet at determinePots");

		Debug.printSolidLine();
		
		ArrayList<Participant> participants = table.getParticipants();
		
		// Make a bet lookup for the participants bet using their position,
		// since the positions starts at 1 instead of 0 we need to add 1 to the size of the array.
		int[] betLookup = new int[participants.size() + 1];
		
		for (Participant participant: participants)
		{
			BetContainer betContainer = participant.getBetContainer();
			
			// Hack: create a case where only the main pot exists.
			//betContainer.setChips(1600);
			//betContainer.setBet(400);
			
			betLookup[participant.getPosition()] = participant.getBetContainer().getBet();
		}
		
		// Determine how much each players bet was.
		for (Participant playingParticipant: participants)
		{
			int playingParticipantBet = playingParticipant.getBetContainer().getBet();
			
			Debug.log(playingParticipantBet, playingParticipant.toString());
		}
		
		Debug.printSolidLine();

		int gameRoundParticipantsSize = gameRoundParticipants.size();
		
		for (int i = 0; i < gameRoundParticipantsSize; i++)
		{
			Participant gameRoundParticipant = gameRoundParticipants.get(i);
			
			Debug.log(gameRoundParticipant, "gameRoundParticipant");
			
			int gameRoundPosition = gameRoundParticipant.getPosition();
			
			if (betLookup[gameRoundPosition] > 0)
			{
				Pot pot = table.getNextPot();
				
				for (Participant participant: participants)
				{
					int position = participant.getPosition();
					
					// If there bet is set to 0 they will not belong to any next pot to come.
					// So we can ignore these participants.
					if (betLookup[position] == 0)
					{
						// Removing them caused JAVA to crash because then we are removing something out
						// of the array list that we are looping.
						continue;
					}
					
					participant.setPotIndex(table.getCurrentPotIndex());
					
					if (participant == gameRoundParticipant)
					{
						pot.addChips(betLookup[position]);
					}
					else
					{
						if (betLookup[position] > betLookup[gameRoundPosition])
						{
							pot.addChips(betLookup[gameRoundPosition]);
							
							betLookup[position] -= betLookup[gameRoundPosition];
						}
						else
						{
							pot.addChips(betLookup[position]);
							
							betLookup[position] = 0;
						}
					}
				}
				
				Debug.log(pot.getChips(), "pot chips");
				
				betLookup[gameRoundPosition] = 0;
				
				Debug.printLine();
			}
		}
	}
	
	public void determineWinners()
	{
		ArrayList<Combination> combinations = new ArrayList<Combination>();
		
		for (Participant participant: gameRoundParticipants)
		{
			Combination combination = new Combination();
			combination.addCards(table.getBoard());
			combination.addCards(participant.getHand());
			combination.calculateBestCombination();
			
			combination.setParticipant(participant);
			participant.setLastCombination(combination);
			
			combinations.add(combination);
		}
		
		winners = new ArrayList<Participant>();
		
		Combination best = combinations.get(0);

		winners.add(best.getParticipant());
		
		for (int i = 1, size = combinations.size(); i < size; i++)
		{
			Combination combination = combinations.get(i);
			
			switch (combination.compare(best))
			{
				case WIN:
					
					winners = new ArrayList<Participant>();
					
					best = combination;

					winners.add(combination.getParticipant());
					
					break;
				
				case TIE:
					
					winners.add(combination.getParticipant());
					
					break;
			}
		}
		
		// Hack: force one winner.
		//winners = new ArrayList<Participant>(winners.subList(0, 1));
		
		// Hack: force multiple winners.
		//winners = new ArrayList<Participant>(gameRoundParticipants.subList(0, 2));
		
		Collections.sort(winners, participantBetSorter);
		
		Debug.log(winners, "winners");
	}

	// Calculates the amount of chips/cash the winner(s) should receive.
	public void payout()
	{
		determineWinners();
		
		determinePots();
		
		if (!Debug.isTextual())
		{
			ServerRemote.getInstance().showGameRoundWinners(getGameId(), winners);
		}
		
		int size = winners.size();
		
		// Calculates the total amount of chips still in the game.
		// It is only used as debug information if some chips happen to (dis)appear.
		int total = 0;
		
		total += table.getMainPotChips();
		
		for (Participant p: table.getParticipants())
		{
			total += p.getBetContainer().getChips();
		}
		
		for (SidePot pot: table.getSidePots())
		{
			total += pot.getChips();
		}

		Debug.printSolidLine();
		
		Debug.log(total, "total chips at payout");

		Debug.printSolidLine();
		
		int participantsInPot[] = new int[table.getSidePots().size() + 1];
		
		// Split pot
		if (size > 1)
		{
			// NOTE: per split pot one chip could disappear, but this is the desired result.
			// Because if you would split 1000 in to 3 parts, we think it is fairer to the players to give
			// all of them the same amount 333 and not one 334 and the others 333.
			
			
			
			int counter = 0;
			
			for (SidePot sidePot: table.getSidePots())
			{
				counter++;
	
				for (Participant winner: winners)
				{
					if (winner.getPotIndex() >= counter)
					{
						participantsInPot[counter]++;
					}
				}
			}
			
			for (Participant winner: winners)
			{
				BetContainer betContainer = winner.getBetContainer();		
				
				betContainer.addChips(table.getMainPotChips() / size);
			    winner.getGameStats().calculateWinLoss(table.getMainPotChips() / size);
				counter = 0;
				
				for (SidePot sidePot: table.getSidePots())
				{
					counter++;
					
					Debug.log(sidePot.getChips(), "side pot chips");
					Debug.log(winner.getPotIndex(), "side pot index");
					
					
					
					
					if (winner.getPotIndex() >= counter)
					{
						betContainer.addChips(sidePot.getChips() / participantsInPot[counter]);
						winner.getGameStats().calculateWinLoss(sidePot.getChips() / participantsInPot[counter]);
						
						sidePot.setPayed(true);
						
					}
				}
			}
			
			for (SidePot sidePot: table.getSidePots())
			{
				
			  if(!sidePot.hasPayed())
			  {
				  for (Participant participant: table.getParticipants())
					{
						if (participant.getPotIndex() >= counter)
						{
							participantsInPot[counter]++;
						}
					}
					
					for (Participant participant: table.getParticipants())
					{
						
						if (participant.getPotIndex() >= counter)
						{
							participant.getBetContainer().addChips(sidePot.getChips() / participantsInPot[counter]);
							participant.getGameStats().calculateWinLoss(sidePot.getChips() / participantsInPot[counter]);
						}
					}
			  }
				
				
			}
		}
		// One winner.
		else
		{
			BetContainer betContainer = winners.get(0).getBetContainer();
			int counter = 0;
			
			for (SidePot sidePot: table.getSidePots())
			{
				
				counter++;
				if (winners.get(0).getPotIndex() >= counter)
				{
					betContainer.addChips(sidePot.getChips());
					winners.get(0).getGameStats().calculateWinLoss(sidePot.getChips());
					
				}
				else
				{
					for (Participant participant: table.getParticipants())
					{
						if (participant.getPotIndex() >= counter)
						{
							participantsInPot[counter]++;
						}
					}
					
					for (Participant participant: table.getParticipants())
					{
						
						if (participant.getPotIndex() >= counter)
						{
							participant.getBetContainer().addChips(sidePot.getChips() / participantsInPot[counter]);
							participant.getGameStats().calculateWinLoss(sidePot.getChips() / participantsInPot[counter]);
						}
					}
				}
			}
			winners.get(0).getGameStats().calculateWinLoss(table.getMainPotChips());
			betContainer.addChips(table.getMainPotChips());
		}
		
		
		
		
		
		Debug.printSolidLine();
		
		for (Participant participant: table.getParticipants())
		{
			if(participant.getBetContainer().getBet()>0)
			{
				PlayerGameStats tempGamestats  = participant.getGameStats();
				
				tempGamestats.calculateWinLoss(-participant.getBetContainer().getBet());
				tempGamestats.increaseRoundsPlayed();
				tempGamestats.addRoundStats(participant.getBetContainer().getBet(),tempGamestats.getRoundWinnings());
				
			}
			
			
			participant.getBetContainer().setBet(0);
			participant.getBetContainer().setStateBet(0);
			//I am not 100% sure but I think the code below will not work in the way we want it to ill look into this later.
			if(participant.getBetContainer().getChips()==0)
			{
				if(participant.getEndPosition()==0)
				{
				  participant.setEndPosition(table.getCurrentEndPosition());
				  table.setCurrentEndPosition(table.getCurrentEndPosition()-1);
				}
			}
		}
		
		
		
		table.emptyPots();
	}
}
